--- import Layout from '../../layouts/Layout.astro'; import { getBlogPostBySlug, getRelatedPosts } from '../../lib/blog'; import { getProductsFromDB } from '../../lib/products'; import ProductCard from '../../components/ProductCard.astro'; import Breadcrumb from '../../components/Breadcrumb.astro'; import { marked } from 'marked'; const { slug } = Astro.params; const post = await getBlogPostBySlug(slug!); if (!post) { return Astro.redirect('/blog'); } const related = await getRelatedPosts(post.category, post.slug, 3); // Convert markdown to HTML, preserving comments marked.use({ breaks: false, gfm: true } as Parameters[0]); const htmlContent = await marked(post.content); // Parse product block markers: // Returns content split into segments: { type: 'html' | 'products', value, products? } interface ContentSegment { type: 'html' | 'products'; html: string; products?: Awaited>; } const productBlockRegex = //g; const segments: ContentSegment[] = []; let lastIndex = 0; let match: RegExpExecArray | null; while ((match = productBlockRegex.exec(htmlContent)) !== null) { // Add preceding HTML if (match.index > lastIndex) { segments.push({ type: 'html', html: htmlContent.slice(lastIndex, match.index) }); } // Parse params like "brand:nike,model:dunk,limit:4" or just "nike-dunk" const paramStr = match[1].trim(); const opts: Record = {}; if (paramStr.includes(':')) { for (const part of paramStr.split(',')) { const [k, v] = part.split(':').map(s => s.trim()); if (k && v) opts[k] = v; } } else { // Simple format: "nike-dunk" โ†’ treat as model search opts.model = paramStr; } try { const products = await getProductsFromDB({ brand: opts.brand, model: opts.model, limit: parseInt(opts.limit || '4'), orderBy: (opts.sort as 'popular' | 'newest' | 'price_asc' | 'price_desc' | 'discount') || undefined, }); segments.push({ type: 'products', html: '', products }); } catch { // If product fetch fails, just skip the marker } lastIndex = match.index + match[0].length; } // Add remaining HTML if (lastIndex < htmlContent.length) { segments.push({ type: 'html', html: htmlContent.slice(lastIndex) }); } const categoryMap: Record = { guides: 'Koopgidsen', deals: 'Deals', comparisons: 'Vergelijkingen', trends: 'Trends', }; const wordCount = post.content.replace(//g, '').replace(/[#*_\[\]()]/g, '').split(/\s+/).length; const readingTime = Math.max(1, Math.round(wordCount / 200)); function formatDate(d: string | null) { if (!d) return ''; return new Date(d).toLocaleDateString('nl-NL', { day: 'numeric', month: 'long', year: 'numeric' }); } const canonicalUrl = `https://sneakerpicks.nl/blog/${post.slug}`; const jsonLd = { '@context': 'https://schema.org', '@type': 'Article', inLanguage: 'nl-NL', headline: post.title, description: post.meta_description || post.excerpt, image: post.cover_image, author: { '@type': 'Organization', name: 'SneakerPicks', url: 'https://sneakerpicks.nl' }, publisher: { '@type': 'Organization', name: 'SneakerPicks', url: 'https://sneakerpicks.nl', logo: { '@type': 'ImageObject', url: 'https://sneakerpicks.nl/logo.svg' }, }, datePublished: post.published_at, dateModified: post.updated_at, mainEntityOfPage: canonicalUrl, }; const breadcrumbLd = { '@context': 'https://schema.org', '@type': 'BreadcrumbList', inLanguage: 'nl-NL', itemListElement: [ { '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sneakerpicks.nl/' }, { '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://sneakerpicks.nl/blog' }, { '@type': 'ListItem', position: 3, name: post.title }, ], }; ---